/*
    SDL - Simple DirectMedia Layer
    Copyright (C) 1997, 1998, 1999  Sam Lantinga

    This library is SDL_free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public
    License along with this library; if not, write to the Free
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Sam Lantinga
    slouken@devolution.com
*/

/* The high-level video driver subsystem */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <SDL/SDL.h>
#include "SDL_sysvideo.h"
#include "SDL_pixels_c.h"

/* Available video drivers */
extern VideoBootStrap WINDIB_bootstrap;
static VideoBootStrap *bootstrap[] = {
	&WINDIB_bootstrap,
	NULL
};
SDL_VideoDevice *current_video;

/* Places to store title and icon text for the app */
static char *wm_title = NULL;
static char *wm_icon  = NULL;
static int offset_x, offset_y;

/* Various local functions */
int SDL_VideoInit(Uint32 flags, void *user_data);
void SDL_VideoQuit(void);

/*
 * Initialize the video and event subsystems -- determine native pixel format
 */
int SDL_VideoInit (Uint32 flags, void *user_data)
{
	SDL_VideoDevice *video;
	const char *driver_name;
	int index;
	int i;
	SDL_PixelFormat vformat;
	Uint32 video_flags;

	/* Select the proper video driver */
	driver_name = getenv("SDL_VIDEODRIVER");
	index = 0;
	video = NULL;
	if ( driver_name != NULL ) {
		if ( strrchr(driver_name, ':') != NULL ) {
			index = atoi(strrchr(driver_name, ';')+1);
		}
		for ( i=0; bootstrap[i]; ++i ) {
			if ( strncmp(bootstrap[i]->name, driver_name,
			             strlen(bootstrap[i]->name)) == 0 ) {
				if ( bootstrap[i]->available() == 0 ) {
					video = bootstrap[i]->create(index, user_data);
					break;
				}
			}
		}
	}
	if ( video == NULL ) {
		for ( i=0; bootstrap[i]; ++i ) {
			if ( bootstrap[i]->available() ) {
				video = bootstrap[i]->create(index, user_data);
				if ( video != NULL ) {
					break;
				}
			}
		}
	}
	if ( video == NULL ) {
		SDL_SetError("No video devices available");
		return(-1);
	}
	current_video = video;

	/* Do some basic variable initialization */
	video->screen = NULL;
	video->shadow = NULL;
	video->visible = NULL;
	video->wm_title = NULL;
	video->wm_icon  = NULL;
	video->offset_x = 0;
	video->offset_y = 0;
	memset(&video->info, 0, (sizeof video->info));

	/* Initialize the video subsystem */
	memset(&vformat, 0, sizeof(vformat));
	if ( video->VideoInit(video, &vformat) < 0 ) {
		SDL_VideoQuit();
		return(-1);
	}

	/* Create a zero sized video surface of the appropriate format */
	video_flags = SDL_SWSURFACE;
	SDL_VideoSurface = SDL_CreateRGBSurface(video_flags, 0, 0,
				vformat.BitsPerPixel,
				vformat.Rmask, vformat.Gmask, vformat.Bmask, 0);
	if ( SDL_VideoSurface == NULL ) {
		SDL_VideoQuit();
		return(-1);
	}
	SDL_PublicSurface = NULL;	/* Until SDL_SetVideoMode() */

	/* If we have a palettized surface, create a default palette */
	if ( SDL_VideoSurface->format->palette ) {
		SDL_DitherColors(SDL_VideoSurface->format->palette->colors,
					SDL_VideoSurface->format->BitsPerPixel);
		video->SetColors(video,
				0, SDL_VideoSurface->format->palette->ncolors);
	}
	video->info.vfmt = SDL_VideoSurface->format;

	/* We're ready to go! */
	return(0);
}

/*
 * Get the current display surface
 */
SDL_Surface *SDL_GetVideoSurface(void)
{
	return(current_video->visible);
}

/*
 * Get the current information about the video hardware
 */
const SDL_VideoInfo *SDL_GetVideoInfo(void)
{
	return(&current_video->info);
}

/*
 * Return a pointer to an array of available screen dimensions for the
 * given format, sorted largest to smallest.  Returns NULL if there are
 * no dimensions available for a particular format, or (SDL_Rect **)-1
 * if any dimension is okay for the given format.  If 'format' is NULL,
 * the mode list will be for the format given by SDL_GetVideoInfo()->vfmt
 */
SDL_Rect ** SDL_ListModes (SDL_PixelFormat *format, Uint32 flags)
{
	SDL_VideoDevice *video = current_video;
	SDL_VideoDevice *this  = current_video;
	SDL_Rect **modes;

	modes = NULL;
	if ( SDL_VideoSurface ) {
		if ( format == NULL ) {
			format = SDL_VideoSurface->format;
		}
		modes = video->ListModes(this, format, flags);
	}
	return(modes);
}

/*
 * Check to see if a particular video mode is supported.
 * It returns 0 if the requested mode is not supported under any bit depth,
 * or returns the bits-per-pixel of the closest available mode with the
 * given width and height.  If this bits-per-pixel is different from the
 * one used when setting the video mode, SDL_SetVideoMode() will succeed,
 * but will emulate the requested bits-per-pixel with a shadow surface.
 */
int SDL_VideoModeOK (int width, int height, int bpp, Uint32 flags) 
{
	Uint8 bit_depths[4][7] = {
		/* 8 bit closest depth ordering */
		{ 0, 8, 16, 15, 32, 24, 0 },
		/* 15,16 bit closest depth ordering */
		{ 0, 16, 15, 32, 24, 8, 0 },
		/* 24 bit closest depth ordering */
		{ 0, 24, 32, 16, 15, 8, 0 },
		/* 32 bit closest depth ordering */
		{ 0, 32, 16, 15, 24, 8, 0 }
	};
	int table, b, i;
	int supported;
	SDL_PixelFormat format;
	SDL_Rect **sizes;

	/* Currently 1 and 4 bpp are not supported */
	if ( bpp < 8 ) {
		return(0);
	}

	/* Search through the list valid of modes */
	memset(&format, 0, sizeof(format));
	supported = 0;
	table = ((bpp+7)/8)-1;
	bit_depths[table][0] = bpp;
	for ( b = 0; !supported && bit_depths[table][b]; ++b ) {
		format.BitsPerPixel = bit_depths[table][b];
		sizes = SDL_ListModes(&format, flags);
		if ( sizes == (SDL_Rect **)0 ) {
			/* No sizes supported at this bit-depth */
			continue;
		} else 
		if ( sizes == (SDL_Rect **)-1 ) {
			/* Any size supported at this bit-depth */
			supported = 1;
			continue;
		} else
		for ( i=0; sizes[i]; ++i ) {
			if ((sizes[i]->w == width) && (sizes[i]->h == height)) {
				supported = 1;
				break;
			}
		}
	}
	if ( supported ) {
		--b;
		return(bit_depths[table][b]);
	} else {
		return(0);
	}
}

/*
 * Get the closest non-emulated video mode to the one requested
 */
static int SDL_GetVideoMode (int *w, int *h, int *BitsPerPixel, Uint32 flags)
{
	SDL_Rect **modes;
	int i, okay;
	Uint8 native_bpp;

	/* Try the original video mode */
	native_bpp = SDL_VideoModeOK(*w, *h, *BitsPerPixel, flags);
	if ( native_bpp == *BitsPerPixel ) {;
		return(1);
	}
	if ( native_bpp > 0 ) {
		*BitsPerPixel = native_bpp;
		return(1);
	}

	/* Enumerate the native video modes, looking for a good size */
	native_bpp = SDL_VideoSurface->format->BitsPerPixel;
	okay = 1;
	modes = SDL_ListModes(SDL_VideoSurface->format, flags);

	/* Check to see if the requested mode is available */
	if ( okay && (modes == (SDL_Rect **)-1) ) {
		/* Uh oh, supports any size, but bit depth failed above (??) */
		SDL_SetError("No video modes supported. (??)");
		okay = 0;
	}
	if ( okay && (modes == (SDL_Rect **)0) ) {
		/* Uh oh, no modes supported at native format (??) */
		SDL_SetError("No video modes supported. (??)");
		okay = 0;
	}
	if ( okay && ((modes[0]->w < *w) || (modes[0]->h < *h)) ) {
		SDL_SetError(
		"Biggest mode (%dx%d) is smaller than desired (%dx%d)",
					modes[0]->w, modes[0]->h, *w, *h);
		okay = 0;
	}
	if ( ! okay ) {
		return(0);
	}

	/* Look for the closest sized mode at native bitdepth */
	for ( i=0; modes[i] && (modes[i]->w > *w) && (modes[i]->h > *h); ++i ) {
		/* keep looking */;
	}
	if ( ! modes[i] || (modes[i]->w < *w) || (modes[i]->h < *h) ) {
		--i;	/* We went too far */
	}

	/* We found the smallest mode >= width x height */
	*w = modes[i]->w;
	*h = modes[i]->h;
	*BitsPerPixel = native_bpp;
	return(1);
}

/* This should probably go somewhere else -- like SDL_surface.c */
static void SDL_ClearSurface(SDL_Surface *surface)
{
	Uint32 black;

	black = SDL_MapRGB(surface->format, 0, 0, 0);
	SDL_FillRect(surface, NULL, black);
	if ((surface->flags&SDL_HWSURFACE) && (surface->flags&SDL_DOUBLEBUF)) {
		SDL_Flip(surface);
		SDL_FillRect(surface, NULL, black);
	}
	SDL_Flip(surface);
}

/*
 * Create a shadow surface suitable for fooling the app. :-)
 */
static void SDL_CreateShadowSurface(int depth)
{
	Uint32 Rmask, Gmask, Bmask;

	/* Allocate the shadow surface */
	if ( depth == (SDL_VideoSurface->format)->BitsPerPixel ) {
		Rmask = (SDL_VideoSurface->format)->Rmask;
		Gmask = (SDL_VideoSurface->format)->Gmask;
		Bmask = (SDL_VideoSurface->format)->Bmask;
	} else {
		Rmask = Gmask = Bmask = 0;
	}
	SDL_ShadowSurface = SDL_CreateRGBSurface(SDL_SWSURFACE,
				SDL_VideoSurface->w, SDL_VideoSurface->h,
						depth, Rmask, Gmask, Bmask, 0);
	if ( SDL_ShadowSurface == NULL ) {
		return;
	}

	/* 8-bit shadow surfaces report that they have exclusive palette */
	if ( depth == 8 ) {
		SDL_ShadowSurface->flags |= SDL_HWPALETTE;
		if ( depth == (SDL_VideoSurface->format)->BitsPerPixel ) {
			memcpy(SDL_ShadowSurface->format->palette->colors,
				SDL_VideoSurface->format->palette->colors,
				SDL_VideoSurface->format->palette->ncolors*
							sizeof(SDL_Color));
		} else {
			SDL_DitherColors(
			SDL_ShadowSurface->format->palette->colors, depth);
		}
	}

	/* If the video surface is fullscreen, the shadow should say so */
	if ( (SDL_VideoSurface->flags & SDL_FULLSCREEN) == SDL_FULLSCREEN ) {
		SDL_ShadowSurface->flags |= SDL_FULLSCREEN;
	}
	/* If the video surface is flippable, the shadow should say so */
	if ( (SDL_VideoSurface->flags & SDL_DOUBLEBUF) == SDL_DOUBLEBUF ) {
		SDL_ShadowSurface->flags |= SDL_DOUBLEBUF;
	}
	return;
}

/*
 * Set the requested video mode, allocating a shadow buffer if necessary.
 */
SDL_Surface * SDL_SetVideoMode (int width, int height, int bpp, Uint32 flags)
{
	SDL_VideoDevice *video = current_video;
	SDL_VideoDevice *this  = current_video;
	SDL_Surface *prev_mode, *mode;
	int video_w;
	int video_h;
	int video_bpp;

	/* Default to the current video bpp */
	if ( bpp == 0 ) {
		flags |= SDL_ANYFORMAT;
		bpp = SDL_VideoSurface->format->BitsPerPixel;
	}

	/* Get a good video mode, the closest one possible */
	video_w = width;
	video_h = height;
	video_bpp = bpp;
	if ( ! SDL_GetVideoMode(&video_w, &video_h, &video_bpp, flags) ) {
		return(NULL);
	}

	/* Check the requested flags */
	/* There's no palette in > 8 bits-per-pixel mode */
	if ( video_bpp > 8 ) {
		flags &= ~SDL_HWPALETTE;
	}
	if ( (flags&SDL_FULLSCREEN) != SDL_FULLSCREEN ) {
		/* There's no windowed double-buffering */
		flags &= ~SDL_DOUBLEBUF;
	}
	if ( (flags&SDL_DOUBLEBUF) == SDL_DOUBLEBUF ) {
		/* Use hardware surfaces when double-buffering */
		flags |= SDL_HWSURFACE;
	}

	/* Clean up any previous video mode */
	if ( SDL_PublicSurface != NULL ) {
		SDL_PublicSurface = NULL;
	}
	if ( SDL_ShadowSurface != NULL ) {
		SDL_Surface *ready_to_go;
		ready_to_go = SDL_ShadowSurface;
		SDL_ShadowSurface = NULL;
		SDL_FreeSurface(ready_to_go);
	}

	/* Try to set the video mode, along with offset and clipping */
	prev_mode = SDL_VideoSurface;

	SDL_VideoSurface = NULL;	/* In case it's freed by driver */
	mode = video->SetVideoMode(this, prev_mode,video_w,video_h,video_bpp,flags);
	SDL_VideoSurface = mode;
	if ( mode != NULL ) {
		if ( (mode->w < width) || (mode->h < height) ) {
			SDL_SetError("Video mode smaller than requested");
			return(NULL);
		}
		mode->offset = 0;
		SDL_ClearSurface(mode);
		offset_x = (mode->w-width)/2;
		offset_y = (mode->h-height)/2;
		mode->offset = offset_y*mode->pitch +
				offset_x*mode->format->BytesPerPixel;
#ifdef DEBUG_VIDEO
  fprintf(stderr,
	"Requested mode: %dx%dx%d, obtained mode %dx%dx%d (offset %d)\n",
		width, height, bpp,
		mode->w, mode->h, mode->format->BitsPerPixel, mode->offset);
#endif
		mode->w = width;
		mode->h = height;
		SDL_SetClipping(mode, 0, 0, 0, 0);
	}

	/* If we failed setting a video mode, return NULL... (Uh Oh!) */
	if ( mode == NULL ) {
		return(NULL);
	}

	/* If we have a palettized surface, create a default palette */
	if ( SDL_VideoSurface->format->palette ) {
		SDL_DitherColors(SDL_VideoSurface->format->palette->colors,
					SDL_VideoSurface->format->BitsPerPixel);
		video->SetColors(this,
				0, SDL_VideoSurface->format->palette->ncolors);
	}
	video->info.vfmt = SDL_VideoSurface->format;

	/* Create a shadow surface if necessary */
	/* There are three conditions under which we create a shadow surface:
		1.  We need a particular bits-per-pixel that we didn't get.
		2.  We need a hardware palette and didn't get one.
		3.  We need a software surface and got a hardware surface.
	*/
	if ( (  !(flags&SDL_ANYFORMAT) &&
			(SDL_VideoSurface->format->BitsPerPixel != bpp)) ||
	     (   (flags&SDL_HWPALETTE) && 
				!(SDL_VideoSurface->flags&SDL_HWPALETTE)) ||
		/* If the surface is in hardware, video writes are visible
		   as soon as they are performed, so we need to buffer them
		 */
	     (   ((flags&SDL_HWSURFACE) == SDL_SWSURFACE) &&
				(SDL_VideoSurface->flags&SDL_HWSURFACE)) ) {
		SDL_CreateShadowSurface(bpp);
		if ( SDL_ShadowSurface == NULL ) {
			SDL_SetError("Couldn't create shadow surface");
			return(NULL);
		}
		SDL_PublicSurface = SDL_ShadowSurface;
	} else {
		SDL_PublicSurface = SDL_VideoSurface;
	}

	/* We're done! */
	return(SDL_PublicSurface);
}

/* 
 * Convert a surface into the video pixel format.
 */
SDL_Surface * SDL_DisplayFormat (SDL_Surface *surface)
{
	Uint32 flags;

	/* Set the flags appropriate for copying to display surface */
	flags  = (SDL_PublicSurface->flags&SDL_HWSURFACE);
	flags |= (surface->flags & (SDL_SRCCOLORKEY|SDL_SRCALPHA));
	return(SDL_ConvertSurface(surface, SDL_PublicSurface->format, flags));
}

/*
 * Update a specific portion of the physical screen
 */
void SDL_UpdateRect(SDL_Surface *screen, Sint32 x, Sint32 y, Uint32 w, Uint32 h)
{
	if ( screen ) {
		SDL_Rect rect;

		/* Perform some checking */
		if ( w == 0 )
			w = screen->w;
		if ( h == 0 )
			h = screen->h;
		if ( (int)(x+w) > screen->w )
			return;
		if ( (int)(y+h) > screen->h )
			return;

		/* Fill the rectangle */
		rect.x = x;
		rect.y = y;
		rect.w = w;
		rect.h = h;
		SDL_UpdateRects(screen, 1, &rect);
	}
}
void SDL_UpdateRects (SDL_Surface *screen, int numrects, SDL_Rect *rects)
{
	int i;

	if ( screen == SDL_ShadowSurface ) {
		/* Blit the shadow surface using saved mapping */
		for ( i=0; i<numrects; ++i ) {
			SDL_LowerBlit(SDL_ShadowSurface, &rects[i], 
					SDL_VideoSurface, &rects[i]);
		}

		/* Fall through to video surface update */
		screen = SDL_VideoSurface;
	}
	if ( screen == SDL_VideoSurface ) {
		SDL_VideoDevice *video = current_video;
		SDL_VideoDevice *this  = current_video;

		/* Update the video surface */
		if ( screen->offset ) {
			for ( i=0; i<numrects; ++i ) {
				rects[i].x += offset_x;
				rects[i].y += offset_y;
			}
			video->UpdateRects(this, numrects, rects);
			for ( i=0; i<numrects; ++i ) {
				rects[i].x -= offset_x;
				rects[i].y -= offset_y;
			}
		} else {
			video->UpdateRects(this, numrects, rects);
		}
	}
}

/*
 * Performs hardware double buffering, if possible, or a full update if not.
 */
int SDL_Flip(SDL_Surface *screen)
{
	/* Copy the shadow surface to the video surface */
	if ( screen == SDL_ShadowSurface ) {
		SDL_Rect rect;

		rect.x = 0;
		rect.y = 0;
		rect.w = screen->w;
		rect.h = screen->h;
		SDL_LowerBlit(SDL_ShadowSurface,&rect, SDL_VideoSurface,&rect);
		screen = SDL_VideoSurface;
	}
	if ( (screen->flags & SDL_DOUBLEBUF) == SDL_DOUBLEBUF ) {
		SDL_VideoDevice *video = current_video;
		SDL_VideoDevice *this  = current_video;
		return(video->FlipHWSurface(this, SDL_VideoSurface));
	} else {
		SDL_UpdateRect(screen, 0, 0, 0, 0);
	}
	return(0);
}

/*
 * Set the display surface colormap.
 */
int SDL_SetColors (SDL_Surface *screen,
				SDL_Color *colors, int firstcolor, int ncolors)
{
	int set_video;
	int alloct_all;

	/* Fail if there's no palette to set */
	if ( screen->format->palette == NULL )
		return(0);

	/* Trying to set too many colors? */
	alloct_all = 1;
	if ( (firstcolor+ncolors) > screen->format->palette->ncolors ) {
		/* My mind tells me no!
		   But my heart, and my hips, cry proceed!
			-- The Simpsons
		 */
		ncolors = screen->format->palette->ncolors - firstcolor;
		alloct_all = 0;
	}

	/* Copy the colors to the surface */
	memcpy(&screen->format->palette->colors[firstcolor], colors,
						ncolors*sizeof(*colors));

	/* Check to see if we need to update the display colormap */
	if ( screen == SDL_ShadowSurface ) {
		/* If display isn't an identity palette, remap pixels */
		set_video = SDL_SetColors(SDL_VideoSurface, colors,
						firstcolor, ncolors);
		if ( ! set_video ) {
			/* Shadow blit mapping is invalid, so mark it so */
			SDL_InvalidateMap(screen->map);
			/* Note:
			   If the app is updating the screen, this could be
			   unecessary work.  For an example, see plasma demo.
			 */
			SDL_UpdateRect(screen, 0, 0, 0, 0);
		}
	} else
	if ( screen == SDL_VideoSurface ) {
		SDL_VideoDevice *video = current_video;
		SDL_VideoDevice *this  = current_video;
		set_video = video->SetColors(this, firstcolor, ncolors);
		if ( ! set_video ) {
			alloct_all = 0;
		}
	}
	SDL_FormatChanged(screen);

	/* We're done! */
	return(alloct_all);
}

/*
 * Clean up the video subsystem
 */
void SDL_VideoQuit (void)
{
	SDL_Surface *ready_to_go;

	if ( current_video ) {
		SDL_VideoDevice *video = current_video;
		SDL_VideoDevice *this  = current_video;

		/* Clean up allocated window manager items */
		if ( SDL_PublicSurface ) {
			SDL_PublicSurface = NULL;
		}

		/* Clean up the system video */
		video->VideoQuit(this);

		/* Free any lingering surfaces */
		ready_to_go = SDL_ShadowSurface;
		SDL_ShadowSurface = NULL;
		SDL_FreeSurface(ready_to_go);
		if ( SDL_VideoSurface != NULL ) {
			ready_to_go = SDL_VideoSurface;
			SDL_VideoSurface = NULL;
			SDL_FreeSurface(ready_to_go);
		}
		SDL_PublicSurface = NULL;

		/* Clean up miscellaneous memory */
		if ( wm_title != NULL ) {
			SDL_free(wm_title);
			wm_title=NULL;
		}
		if ( wm_icon != NULL ) {
			SDL_free(wm_icon);
			wm_icon=NULL;
		}

		/* Finish cleaning up video subsystem */
		video->free(this);
		current_video = NULL;
	}
	return;
}

Uint8 SDL_GetAppState (void)
{
	SDL_VideoDevice *video = current_video;
	Uint8 appState;

	appState = 0;
	if (video->GetAppState) {
		appState = video->GetAppState(video);
	}

	return appState;
}